/*
 * Copyright 2007,2008 Anton Dignoes, Daniel Rampanelli, Martin Palma
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.webduff.store;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperty;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNAuthenticationManager;
import org.tmatesoft.svn.core.io.ISVNEditor;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.diff.SVNDeltaGenerator;

import com.googlecode.webduff.MimeTyper;
import com.googlecode.webduff.authentication.provider.Credential;
import com.googlecode.webduff.io.URI;

/**
 * Class represents a wrapper class for an <code>SVNRepository</code> extending functionalities needed by the WebDuff system. 
 * 
 * @author Anton Dignoes, Daniel Rampanelli, Martin Palma
 *
 */
public class WebDuffSVNRepository {
	
	private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(WebDuffSVNRepository.class);
	
	private SVNRepository theRepository;

	private HashSet<WebDuffSVNRepositoryListener> commitListeners;
	
	/**
	 * Constructor for the wrapper class
	 * @param aRepository
	 * 			denotes the <code>SVNRepository</code> to wrap
	 */
	public WebDuffSVNRepository(SVNRepository aRepository) {
		theRepository = aRepository;
		commitListeners = new HashSet<WebDuffSVNRepositoryListener>();
	}
	
	/**
	 * Method to get the personal <code>URI</code> for a specific user specified by the <code>credential</code>
	 * @param uri
	 * 			denotes the <code>URI</code> to adapt
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user
	 * @return
	 * 			a <code>URI</code> personalized for the specific <code>Credential</code>
	 */
	private URI getUserifiedUri(URI uri, Credential aCredential) {
		// we don't differentiate on username folders (for now)
		// return new File(aCredential.getUsername(), uri).getAbsolutePath();
		return uri;
	}
	
	/**
	 * Method to get the SVN Revision number of a specific <code>Date</code> 
	 * @param aDate
	 * 			denotes the <code>Date</code> of the revision number
	 * @return
	 * 			the revision number at the specific date or if not exact the latest revision before
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong 
	 */
	public long getRevisionByDate(Date aDate) throws SVNException {
		return theRepository.getDatedRevision(aDate);
	}
	
	/**
	 * Method to get the latest revision number of this repository
	 * @return
	 * 			the latest revision number which was commited
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public long getLatestRevision() throws SVNException {
		return theRepository.getLatestRevision();
	}
	
	/**
	 * Method to get the Value of a SVN revision property
	 * @param aRevision
	 * 				the revision for which to get the property
	 * @param propertyName
	 * 				the name of the SVN revision property
	 * @return
	 * 				the value of the property specified by <code>propertyName</code> at revision <code>aRevision</code>
	 * @throws SVNException
	 * 				thrown if something by the SVN Repository went wrong
	 */
	public String getRevisionPropertyValue(long aRevision, String propertyName) throws SVNException {
		return theRepository.getRevisionPropertyValue(aRevision, propertyName);
	}
	
	/**
	 * Method to create a folder at the specific <code>URI</code> for the specific user denoted by the <code>Credential</code>
	 * @param uri
	 * 			denoted the abstract <code>URI</code> of the folder
	 * @param aCredential
	 * 			denotes the date of the user for which to create the folder
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public void createFolder(URI uri, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		String logMessage = "";
		
		synchronized(this) {
			theRepository.setAuthenticationManager(new DefaultSVNAuthenticationManager(null, false, aCredential.getUsername(), null));
			ISVNEditor editor = theRepository.getCommitEditor(logMessage, null);
			editor.openRoot(-1);
			editor.addDir(uri.getPath(), null, -1);
			editor.closeDir();
			editor.closeDir();
			log.trace(editor.closeEdit());
			theRepository.setAuthenticationManager(null);
			
			fireCommitPerformedEvent(theRepository.getLatestRevision());
		}
	}
	
	/**
	 * Method to create a resource at the specific <code>URI</code> for the specific user denoted by the <code>Credential</code>
	 * @param uri
	 * 			denoted the abstract <code>URI</code> of the resource
	 * @param aCredential
	 * 			denotes the date of the user for which to create the resource
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public void createResource(URI uri, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		String logMessage = "";
		
		synchronized(this) {
			theRepository.setAuthenticationManager(new DefaultSVNAuthenticationManager(null, false, aCredential.getUsername(), null));
			ISVNEditor editor = theRepository.getCommitEditor(logMessage, null);
			editor.openRoot(-1);
			editor.addFile(uri.getPath(), null, -1);
			editor.closeFile(uri.getPath(), null);
			editor.closeDir();
			log.trace(editor.closeEdit());
			theRepository.setAuthenticationManager(null);
			
			fireCommitPerformedEvent(theRepository.getLatestRevision());
		}
	}
	
	/**
	 * Method to get a list of all objects of the folder denoted by <code>uri</code> at the specific date <code>aDate</code>
	 * for the user specified by <code>aCredential</code> 
	 * @param uri
	 * 			denotes the abstract <code>URI</code> of the folder
	 * @param aDate
	 * 			denotes the <code>Date</code> for which to get the list 
	 * @param aCredential
	 * 			denotes the data for the user for which to get the list 
	 * @return
	 * 			a list of objects on the specified folder <code>uri</code>, at time <code>aDate</code> for user <code>aCredential</code>
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	@SuppressWarnings("unchecked")
	public Vector<String> listByDate(URI uri, Date aDate, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		Collection entries = theRepository.getDir(uri.getPath(), theRepository.getLatestRevision(), null, (Collection) null);
        Vector<String> entryList = new Vector<String>();
        Iterator iterator = entries.iterator();
        
        while (iterator.hasNext()) {
            SVNDirEntry entry = (SVNDirEntry) iterator.next();
            entryList.add(entry.getName());
        }
        
        return entryList;
	}
	
	/**
	 * Method to get a list of all objects of the folder denoted by <code>uri</code> of the specific revision <code>aRevision</code>
	 * for the user specified by <code>aCredential</code> 
	 * @param uri
	 * 			denotes the abstract <code>URI</code> of the folder
	 * @param aRevision
	 * 			denotes the <code>Revision number</code> for which to get the list
	 * @param aCredential
	 * 			denotes the data for the user for which to get the list
	 * @return
	 * 			a list of objects on the specified folder <code>uri</code>, of revision <code>aRevision</code> for user <code>aCredential</code>
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	@SuppressWarnings("unchecked")
	public Vector<String> listByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		Collection entries = theRepository.getDir(uri.getPath(), aRevision, null, (Collection) null);
        Vector<String> entryList = new Vector<String>();
        Iterator iterator = entries.iterator();
        
        while (iterator.hasNext()) {
            SVNDirEntry entry = (SVNDirEntry) iterator.next();
            entryList.add(entry.getName());
        }
        
        return entryList;
	}
	
	/**
	 * Method to get the creation date of an object of the latest revision 
	 * @param uri
	 * 			denotes the <code>URI</code> of the object
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user 
	 * @return
	 * 			the <code>Date</code> on which the object was created
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public Date getCreationDate(URI uri, Credential aCredential) throws SVNException {
		return getCreationDateByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to get the creation date of an object of the specified revision
	 * @param uri
	 * 			denotes the <code>URI</code> of the object
	 * @param aRevision
	 * 			denotes the revision number of the object
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user
	 * @return
	 * 			the <code>Date</code> on which the object was created
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	@SuppressWarnings("unchecked")
	public Date getCreationDateByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		Vector<SVNLogEntry> revisionsForUri = new Vector<SVNLogEntry>(theRepository.log(new String[] { uri.getPath() }, null, 0L, aRevision, false, false));
		SVNLogEntry firstRevision = revisionsForUri.firstElement();
		return firstRevision.getDate();
	}
	
	/**
	 * Method to get the date on which the object specified by <code>uri</code> was modified last 
	 * @param uri
	 * 			denotes the <code>URI</code> of the object
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the <code>Date</code> on which the object was modified last
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public Date getLastModified(URI uri, Credential aCredential) throws SVNException {
		return getLastModifiedByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to get the date on which die object specified by <code>uri</code> of revision <code>aRevision</code> was modified last
	 * @param uri
	 * 			denotes the <code>URI</code> of the object
	 * @param aRevision
	 * 			denotes the revision number of the object
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the <code>Date</code> on which the object was modified last
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public Date getLastModifiedByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		SVNDirEntry entry = theRepository.info(uri.getPath(), aRevision);
		return entry.getDate();
	}
	
	/**
	 * Method to get the content of a resource as an <code>InputStream</code>
	 * @param uri
	 * 			denotes the <code>URI</code> of the resource
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation 
	 * @return
	 * 			an <code>InputStream</code> representing the content of the resource
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public InputStream getResourceContent(URI uri, Credential aCredential) throws SVNException {
		return getResourceContentByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to get the content of a resource of revision <code>aRevision</code> as an <code>InputStream</code>
	 * @param uri
	 * 			denotes the <code>URI</code> of the resource
	 * @param aRevision
	 * 			denotes the revision number of the resource
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation 
	 * @return
	 * 			an <code>InputStream</code> representing the content of the resource
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public InputStream getResourceContentByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		theRepository.getFile(uri.getPath(), aRevision, null, out);
		return new ByteArrayInputStream(out.toByteArray());
	}
	
	/**
	 * Method to get the length of the resource in bytes
	 * @param uri
	 * 			denotes the <code>URI</code> of the resource
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the length of the resource in bytes
	 * @throws SVNException	
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public long getResourceLength(URI uri, Credential aCredential) throws SVNException {
		return getResourceLengthByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to get the length of the resource at revision <code>aRevision</code> in bytes
	 * @param uri
	 * 			denotes the <code>URI</code> of the resource
	 * @param aRevision
	 * 			denotes the revision number of the resource
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the length of the resource in bytes
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public long getResourceLengthByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		SVNDirEntry entry = theRepository.info(uri.getPath(), aRevision);
		return entry.getSize();
	}
	
	/**
	 * Method to check whether an object is a folder
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation 
	 * @return
	 * 			<code>true</code> if the object is a folder, <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean isFolder(URI uri, Credential aCredential) throws SVNException {
		return isFolderByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to check whether an object is a folder
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aRevision
	 * 			denotes the revision number to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			<code>true</code> if the object is a folder, <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean isFolderByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		SVNNodeKind nodeKind = SVNNodeKind.UNKNOWN;
		nodeKind = theRepository.checkPath(uri.getPath(), aRevision);
		return (nodeKind == SVNNodeKind.DIR);
	}
	
	/**
	 * Method to check whether an object is a resource
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation 
	 * @return
	 * 			<code>true</code> if the object is a resource, <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean isResource(URI uri, Credential aCredential) throws SVNException {
		return isResourceByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to check whether an object is a resource
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aRevision
	 * 			denotes the revision number to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			<code>true</code> if the object is a resource, <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean isResourceByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		SVNNodeKind nodeKind = SVNNodeKind.UNKNOWN;
		nodeKind = theRepository.checkPath(uri.getPath(), aRevision);
		return (nodeKind == SVNNodeKind.FILE);
	}
	
	/**
	 * Method to check if an object exists 
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			<code>true</code> if the object exists <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean objectExists(URI uri, Credential aCredential) throws SVNException {
		return objectExistsByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to check whether an object existed at revision <code>aRevision</code> 
	 * @param uri
	 * 			denotes the <code>URI</code> to check
	 * @param aRevision
	 * 			denotes the revision number to check
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			<code>true</code> if the object exists <code>false</code> otherwise
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public boolean objectExistsByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		return (theRepository.checkPath(uri.getPath(), aRevision) != SVNNodeKind.NONE);
	}
	
	/**
	 * Method to remove an object 
	 * @param uri
	 * 			denotes the <code>URI</code> of the object to remove
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public void removeObject(URI uri, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		String logMessage = "";
		
		synchronized(this) {
			theRepository.setAuthenticationManager(new DefaultSVNAuthenticationManager(null, false, aCredential.getUsername(), null));
			ISVNEditor editor = theRepository.getCommitEditor(logMessage, null);
			editor.openRoot(-1);
			editor.deleteEntry(uri.getPath(), -1);
			editor.closeDir();
			log.trace(editor.closeEdit());
			theRepository.setAuthenticationManager(null);
					
			fireCommitPerformedEvent(theRepository.getLatestRevision());
		}
	}
	
	/**
	 * Method to get the mime-type of an object as a String
	 * @param uri
	 * 			denotes the <code>URI</code> of the object 
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the mime-type of the specified object 
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public String getMimeType(URI uri, Credential aCredential) throws SVNException {
		return getMimeTypeByRevision(uri, -1, aCredential);
	}
	
	/**
	 * Method to get the mime-type of an object at a specific revision as a String 
	 * @param uri
	 * 			denotes the <code>URI</code> of the object 
	 * @param aRevision
	 * 			denotes the revision number of the object for which to get the info
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @return
	 * 			the mime-type of the specified object
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public String getMimeTypeByRevision(URI uri, long aRevision, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		if(isFolderByRevision(uri, aRevision, aCredential)) {
			return MimeTyper.FOLDER_MIMETYPE;
		}
		
		Map<String, String> fileProperties = new HashMap<String, String>();
		theRepository.getFile(uri.getPath(), aRevision, fileProperties, (OutputStream) null);
		if(fileProperties.containsKey("svn:mime-type")) {
			return fileProperties.get("svn:mime-type");
		}
		return null;
	}
	
	/**
	 * Method to set the content of a specific resource
	 * @param uri
	 * 			denotes the <code>URI</code> of the resource
	 * @param content
	 * 			denotes the <code>InputStream</code> of the content
	 * @param contentType
	 * 			denotes the mime-type of the content
	 * @param characterEncoding
	 * 			denotes the character encoding of the content
	 * @param aCredential
	 * 			denotes the <code>Credential</code> of the user requesting the operation
	 * @throws SVNException
	 * 			thrown if something by the SVN Repository went wrong
	 */
	public void setResourceContent(URI uri, InputStream content, String contentType, String characterEncoding, Credential aCredential) throws SVNException {
		uri = getUserifiedUri(uri, aCredential);
		String logMessage = "";
		
		synchronized(this) {
			theRepository.setAuthenticationManager(new DefaultSVNAuthenticationManager(null, false, aCredential.getUsername(), null));
			ISVNEditor editor = theRepository.getCommitEditor(logMessage, null);
			editor.openRoot(-1);
			editor.openFile(uri.getPath(), -1);
			editor.applyTextDelta(uri.getPath(), null);
			
			SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();
			String checksum = deltaGenerator.sendDelta(uri.getPath(), content, editor, true);
			editor.closeFile(uri.getPath(), checksum);
			
			// add MIME type to file 
			editor.changeFileProperty(uri.getPath(), SVNProperty.MIME_TYPE, contentType);

			editor.closeDir();
			log.trace(editor.closeEdit());
			theRepository.setAuthenticationManager(null);
			
			fireCommitPerformedEvent(theRepository.getLatestRevision());
		}
	}
	
	/**
	 * Method to add a Listener which wants to be notified on a commit 
	 * @param newListener
	 * 			denotes the <code>WebDuffSVNRepositoryListener</code> to add/register
	 */
	public void addCommitListener(WebDuffSVNRepositoryListener newListener) {
		synchronized(commitListeners) {
			commitListeners.add(newListener);
		}
	}
	
	/**
	 * Method to delete an already added <code>WebDuffSVNRepositoryListener</code>
	 * @param newListener
	 * 			denotes the <code>WebDuffSVNRepositoryListener</code> to remove/unregister
	 */
	public void removeCommitListener(WebDuffSVNRepositoryListener newListener) {
		synchronized(commitListeners) {
			commitListeners.remove(newListener);
		}
	}
	
	@SuppressWarnings("unchecked")
	private void fireCommitPerformedEvent(long aRevision) throws SVNException {
		synchronized(commitListeners) {
			final SVNLogEntry theEntry = (SVNLogEntry) theRepository.log(null, null, aRevision, aRevision, true, true).iterator().next();
			
			for(WebDuffSVNRepositoryListener aListener : commitListeners) {
				Credential monitoringCredential = aListener.getMonitoringCredential();
				
				final WebDuffSVNRepositoryListener aFinalListener = aListener;
				
				if(credentialMonitoringAgrees(theEntry, monitoringCredential)) {
					new Thread() {
						public void run() {
							aFinalListener.commitPerformed(theEntry);
						}
					}.start();
				}
			}
		}
	}
	
	private boolean credentialMonitoringAgrees(SVNLogEntry anEntry, Credential aCredential) {
		if(aCredential == null || anEntry.getAuthor().equals(aCredential.getUsername())) {
			return true;
		}
		
		return false;
	}

}